/**
	License:		GNU General Public License version 3 (see license.txt, also available online at http://www.gnu.org/licenses/gpl-3.0.html)
	Authors:		OrbitalLab (http://www.orbitallab.ru/dee0xd/), 2008-2009

	File:			dee0xd.d
	Description:	some core stuff
	Date:			19.10.2008 by Digited
**/
module dee0xd.Core;

import
	tango.sys.Environment,
	tango.sys.Process,

	tango.util.PathUtil,
	tango.util.ArgParser,

	tango.text.Util,
	tango.text.convert.Layout,
	tango.io.stream.Lines,

	tango.io.Console,
	tango.io.Stdout,

	tango.time.StopWatch,

	dee0xd.Version,
	dee0xd.Util,
	dee0xd.Log,

	dee0xd.compilers.ICompiler,
	dee0xd.compilers.DMD;
	
version( dee0xd_gui ) import dee0xd.gui.Editor;


//---------------------------------------------------------------------------------------------------
/// The Main Class
//---------------------------------------------------------------------------------------------------
class Dee0xD
{
	public:
		char[][char[]] params, envVars;
		StopWatch timer;
		ICompiler compiler;
		bool guiRunning;

		//---------------------------------------------------------------------------------------------------
		/// Constructor - looks at application arguments and does something, depending on the weather on Mars and some random numbers
		//---------------------------------------------------------------------------------------------------
		this( char[][] args )
		{
			// -------------- command line saving ----------------------------------------------------
			char[] cmdLine;

			foreach( arg; args )
				if( arg.length )
					cmdLine ~= arg ~ " ";

			if( cmdLine.length )
			{
				params["command"] = cmdLine;
				
				if( args.length > 1 && args[1] != "repeat" && !containsPattern( cmdLine, " --forget " ))
					try
					{
						scope last_dxd = new File( "last.dxd", File.WriteCreate );
						last_dxd.write( cmdLine[0..$-1] );    // do not write last ' '
						last_dxd.close;
					}
					catch( Exception e )
						log.error( e.msg );
			}
			//----------------------------------------------------------------------------------------

			params["name"]				= "output";
			params["outdir"]			= "";
			params["bindir"]			= "bin";
			params["-p"]				= "512"; // I don't want users to have problems with static lib linking, if they don't know about page size issue. Just setting pagesize bigger won't do any trouble (I hope), but will enable linking BIG libs
			envVars						= Environment.get;

			version( Windows )
				params["path"] = GetApplicationPath;
			else
				params["path"] = envVars["PWD"];

			compiler = DMD();

			if( args.length > 1 )
			{
				switch( args[1] )
				{
					case "help":
					case "--help":
						DisplayHelp;
						break;

					case "build":
						timer.start;
						Execute( Build( args[2..$] ));
						log.trace( Format( "\n\nDone in {} seconds ^____^\n", timer.stop ));
						break;

					case "clean":
						timer.start;
						Clean( args[2..$] );
						log.trace( Format( "\n\nDone in {} seconds ^____^\n", timer.stop ));
						break;

					case "rebuild":
						timer.start;
						Clean( args[2..$] );
						Execute( Build( args[2..$] ));
						log.trace( Format( "\n\nDone in {} seconds ^____^\n", timer.stop ));
						break;

					case "repeat":
						timer.start;

						if( args.length > 2 )
							log.warn( "repeat ignores parameters!" );

						Repeat();
						log.trace( Format( "\n\nDone in {} seconds ^____^\n", timer.stop ));
						break;

					default:
						Cout( args[1] ~ ": do what?!... O_o" ).newline;
						break;
				}
			}
			else
				DisplayHelp;
		}


		//---------------------------------------------------------------------------------------------------
		/// Execute commands
		//---------------------------------------------------------------------------------------------------
		version( dxd_test )
		{
			void Execute( char[][][] commands )
			{
				log.warn( "testing output:" );

				foreach( command; commands )
					log.warn( Format( "{}", command ));
			}
		}
		else
		{
			void Execute( char[][][] commands )
			{
				Process.Result result;

				scope p = new Process( true, null );
				p.setWorkDir( params["path"] ).setGui( true ).setRedirect( Redirect.ErrorToOutput | Redirect.Output );

				foreach( command; commands )
				{
					if( command.length == 0 )
						continue;

					log.trace( Format( "{}", command ));

					try
					{
						if( command[0] == dxd_internal_rename )
						{
							if( command.length < 3 )
								throw new Exception( "dxd internal error: not enough paramenters to rename" );
							else
							{
								auto fp = new FilePath( command[1] );
								
								if( fp.exists && !fp.isFolder )
									fp.rename( command[2] );
								else
									throw new Exception( "dxd internal error: " ~ command[1] ~ " is not a valid file to rename" );
							}
						}
						else
						{
							if( command.length < 2 )
								p.setArgs( command[0], null ).execute;
							else
								p.setArgs( command[0], command[1..$] ).execute;
							
							foreach( line; new Lines!(char)( p.stdout ))
								if( line.length )
									log.warn( line );

							result = p.wait;
						}
					}
					catch( Exception e )
					{
						log.error( "dee0xd catched an error: " ~ e.msg );
						return;
					}

					if( result.reason == Process.Result.Error )
					{
						log.error( result.toString );
						return;
					}
				}
			}
		}


		//---------------------------------------------------------------------------------------------------
		/// Build whatever we can
		//---------------------------------------------------------------------------------------------------
		char[][][] Build( char[][] args )
		{
			bool stlib, group, seplink, nolink;

			if( args.length )
			{
				scope argEngine = new ArgParser;
				//---------------------------------------------
				argEngine.bind( "-", "l",	( char[] value ) { params["-l"] ~= value ~ " "; } );
				argEngine.bind( "-", "I",	( char[] value ) { params["-I"] ~= " -I" ~ value; } );
				argEngine.bind( "-", "v",	( char[] value ) { params["-v"] ~= value ~ ";";	} );
				argEngine.bind( "-", "L",	( char[] value ) { params["-L"] ~= value ~ ";"; } );
				argEngine.bind( "-", "s",	( char[] value ) { params["-s"] ~= value ~ ";"; } );
				argEngine.bind( "-", "p",	( char[] value ) { params["-p"] = value; } );
				argEngine.bind( "-", "g",	( char[] value ) { params["-g"] = "true"; } );
				argEngine.bind( "-", "d",	( char[] value ) { params["-d"] = "true"; } );
				argEngine.bind( "-", "h",	( char[] value ) { params["-h"] = "true"; } );
				argEngine.bind( "-", "o",	( char[] value ) { params["-o"] = "true"; } );
				argEngine.bind( "-", "i",	( char[] value ) { params["-i"] = "true"; } );
				argEngine.bind( "-", "r",	( char[] value ) { params["-r"] = "true"; } );
				argEngine.bind( "-", "u",	( char[] value ) { params["-u"] = "true"; } );

				argEngine.bind( "-", "c",
					(char[] value )
					{
						switch( value )
						{
//							case "ldc": case "LDC": case "llvmdc": case "LLVMDC":
//							compiler = LDC();
//							break;

							case "dmd": case "DMD":
								if( !( compiler && cast(DMD)compiler))
									compiler = DMD();
								break;
								
							case "ldmd":
								if( !( compiler && cast(DMD)compiler ))
									compiler = DMD();

								compiler.cmdCompiler = ["ldmd"];
								compiler.cmdLinkExe = ["gcc"];
								compiler.cmdLinkLib = ["ar","rcs"];
								break;
								
							case "gdmd":
								if( !( compiler && cast(DMD)compiler ))
									compiler = DMD();

								compiler.cmdCompiler = ["gdmd"];
								compiler.cmdLinkExe = ["gcc"];
								compiler.cmdLinkLib = ["ar","rcs"];
								break;

							default:
								log.warn( "Unsupported compiler: " ~ value );
								break;
						}
					});
					
				argEngine.bind( "--", "verbose",( char[] value ) { log.level = Level.Trace;		} );
				argEngine.bind( "--", "forget", ( char[] value ) { params["forget"] = "true";	} );
				argEngine.bind( "--", "rename",	( char[] value ) { params["--rename"] = "true";	} );

				argEngine.bind( "", "outdir=",	( char[] value ) { params["outdir"]	= value;	} );
				argEngine.bind( "", "bindir=",	( char[] value ) { params["bindir"]	= value;	} );
				argEngine.bind( "", "name=",	( char[] value ) { params["name"]	= value;	} );
				argEngine.bind( "", "lib",		( char[] value ) { params["lib"]	= "true";	stlib = true;	} );
				argEngine.bind( "", "group",	( char[] value ) { params["group"]	= "true";	group = true;	} );
				argEngine.bind( "", "seplink",	( char[] value ) { params["seplink"]= "true";	seplink = true;	} );
				argEngine.bind( "", "nolink",	( char[] value ) { params["nolink"]	= "true";	nolink = true;	} );
				argEngine.bind( "", "check",	( char[] value ) { params["check"]	= "true";	} );
				argEngine.bind( "", "add",		( char[] value ) { params["add"]	~= value ~ ";";	} );

				argEngine.bindDefault(( char[] value, uint count ) { Cout( "unknown argument: " ~ value ).newline; } );
				//---------------------------------------------
				try
				{
					argEngine.parse( args );
				}
				catch(Exception e)
				{
					log.error( "error parsing command line: " ~ e.msg );
				}
			}

			//----------- checks ------------------------------------------
			if( !group && seplink )
			{
				log.warn( "seplink is meaningless without group, ignoring... o_O" );
				params["seplink"] = "false";
			}

			if( seplink && nolink )
			{
				log.warn( "seplink and nolink are mutually exclusive, using nolink... O_o" );
				params["seplink"] = "false";
			}

			if( nolink && stlib )
			{
				log.warn( "lib is meaningless with nolink, ignoring... o_O" );
				params["lib"] = "false";
			}

			//----------- search modules ----------------------------------
			log.trace( params["command"] );

			if( !( "-s" in params ))
				params["-s"] = "";

			auto modules = SearchFiles( params["path"], "", ".d", true, params["-s"] );

			if( modules.length )
				log.trace( Format( "{} {} found...", modules.length, (( modules.length == 1 ) ? "module" : "modules" )));
			else
			{
				log.warn( "No modules found!" );
				return null;
			}
/*
            rules for checking:
            
            1. Try to find output file. If found, get date.
            2. Cycle modules, if link - cycle objs also, get dates, compare with output and objs, make data:
            2.1 if there are modules older than output
            2.2 list of modules, older that objs (or if no obj), and list of younger
            
            if superlib, no objs, just use comparing of objs to output. If !output, recompile all. If some are older, compile with 1 command, using objs of younger modules
            else if no modules older than output( and output presents ), do nothing.
            
            
            
			if( "check" in params )
			{
				FilePath[] upToDates;

				if( !( group && stlib ))	// we don't have to check for object files in group+lib case
				{
					//----------- check up-to-date modules ------------------------
					upToDates = FindUpToDate( modules, params["path"], params["bindir"], compiler.ObjExt );

					if( upToDates.length )
					{
						log.trace( Format( "{} modules are up-to-date...", upToDates.length ));

						char[] utds;

						foreach( utd; upToDates )
							utds ~= utd.toString ~ " ";

						params["-l"] = utds ~ params["-l"];
					}
				}

				//------------ check if output file exists --------------------
				// if there's no modules to compile (all are up-to-date) and output exists, do nothing.
				char[] outputFileName = params["path"] ~ '/' ~ params["outdir"] ~ params["name"];

				if( "lib" in params )
					outputFileName ~= compiler.LibExt;
				else
					outputFileName ~= compiler.ExeExt;

				auto outputFile = new FilePath( normalize( outputFileName ));

				if( outputFile.exists )
					if( modules.length == 0 )
					{
						log.warn( "output file is up to date, do nothing." );
						return null;
					}
			}
*/
			return compiler.GenCommands( params, modules );
		}


		//---------------------------------------------------------------------------------------------------
		/// remove all files from output directory, on critical hit - format HDD
		//---------------------------------------------------------------------------------------------------
		void Clean( char[][] args )
		{
			if( args.length )
			{				
				scope argEngine = new ArgParser;
				//---------------------------------------------
				argEngine.bind( "", "outdir=",	( char[] value ) { params["outdir"]	= value;	} );
				argEngine.bind( "", "bindir=",	( char[] value ) { params["bindir"]	= value;	} );
				argEngine.bind( "", "name=",	( char[] value ) { params["name"]	= value;	} );
				//---------------------------------------------
				try
				{
					argEngine.parse( args );
				}
				catch(Exception e)
				{
					log.error( "error parsing command line: " ~ e.msg );
				}
			}
				
			// delete .maps 
			auto maps = SearchFiles( params["path"], "", ".map", false );
				
			foreach( map; maps )
				map.remove;
				
			char[]* bindir = "bindir" in params;
			char[] objDir;
			
			if( bindir && (*bindir).length )
				objDir = *bindir;
			else
				objDir = params["path"];

			// delete .objs
			auto objs = SearchFiles( objDir, "", ".obj", true );
			
			foreach( obj; objs )
				obj.remove;
		}


		//---------------------------------------------------------------------------------------------------
		/// Repeat what was saved to last.dxd
		//---------------------------------------------------------------------------------------------------
		void Repeat()
		{
			scope last_dxd_path = new FilePath( "last.dxd" );

			if( last_dxd_path.exists && !last_dxd_path.isFolder )
			{
				// load, read saved commmand, remove self as first param (self must be first param!) and, if not empty, run it
				try
				{
					scope last_dxd = new File( "last.dxd", File.ReadExisting );
					bool opened = true;
				
					foreach( line; new Lines!(char)( last_dxd.input ))
						if( line.length )
						{
							last_dxd.close;
							opened = false;

							if( !containsPattern( line, " repeat " ))
								scope cool_tool = new Dee0xD( split( line, " " ));
							else
								log.error( "last.dxd contains \" repeat \", there's a risk of infinite recursion!" );

							break;
						}
						
					if( opened )
						last_dxd.close;
				}
				catch( Exception e )
					log.error( e.msg );
			}
		}
		
		//---------------------------------------------------------------------------------------------------
		/// print description string
		//---------------------------------------------------------------------------------------------------
		void DisplayHelp()
		{
			Cout( description ).newline;
		}
}


//---------------------------------------------------------------------------------------------------
// program entry point
//---------------------------------------------------------------------------------------------------
int main( char[][] args )
{
	scope cool_tool = new Dee0xD( args ); // ^____^
	return 0;
}

unittest
{
	Cout( "Unittested!" ).newline;
}